/******************************************************************************************************
Copyright (C) 2013 Mestrelab Research S.L. All rights reserved.

This file is part of the Mnova scripting toolkit.

Authorized users of MNova Software may use this file freely, but this file is provided AS IS
with NO WARRANTY OF ANY KIND, INCLUDING THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE.
*****************************************************************************************************/

/*globals Dir, settings*/
/*jslint plusplus: true, indent: 4*/

function PurityCalculator() {
	"use strict";
	var purityReferenceCompoundFileKey = "Purity Calculator/Purity Reference Compound File",
		purityCalcParamsFileKey = "Purity Calculator/Purity Calc Params File",
		refCompFile = Dir.application() + "/scripts/config/referenceCompounds.csv",
		calcParamsFile = Dir.home() + "/purityCalcParams.txt",
		purityReferenceCompoundFile = settings.value(purityReferenceCompoundFileKey, refCompFile),
		purityCalcParamsFile = settings.value(purityCalcParamsFileKey, calcParamsFile),
		SHOW_ERROR_BOUNDS = false;	//whether or not to show the ±2%

	PurityCalculatorObj(purityReferenceCompoundFile, purityCalcParamsFile, purityCalcParamsFileKey, SHOW_ERROR_BOUNDS, purityReferenceCompoundFileKey);
}

//Main function
function PurityCalculatorObj(aPurityReferenceCompoundFile, aPurityCalcParamsFile, aPurityCalcParamsFileKey, aShowErrorBounds, aPurityReferenceCompoundFileKey) {
	"use strict";
    calculatePurity();

/**
 * Class to encapsulate the properties required to calculate the purity (and salt molar ratio) of a compound.
 */
	function PurityCalcParams(fidDir) {

		this.sampleMolWeight = NaN;
		this.sampleWeight = NaN;
		this.refCompound = 0;
		this.refCmpdName = "";
		this.refMolWeight = NaN;
		this.refPurity = NaN;
		this.refWeight = NaN;
		this.refAssignment = 0;
		this.sampleAssignments = [];
		this.saltAssignment = 0;
		this.useGSD = false;
		this.reportOnSeparatePage = false;


		/**
		 * Calculates the average purity over ll sample integrals.
		 */
		this.averagePurity = function () {

			var i, purity,
				averagePurity = 0.0,
				purityVals = [];
			for (i = 0; i < this.sampleAssignments.length; i++) {

				purity = this.purityOf(this.sampleAssignments[i]);

				if (!isNaN(purity)) {
					purityVals.push(purity);
					averagePurity += purity;
				}
			}
			if (!purityVals) {
				throw "No sample peaks have been assigned";
			}
			averagePurity /= purityVals.length;
			return averagePurity;
		};

		/**
		 * Calculates the purity for one sample integral.
		 * @param assignment - an Assignment object containing the sample integral and nuclide count.
		 */
		this.purityOf = function(assignment) {

			//check params
			if(isNaN(this.sampleMolWeight)) throw "Sample mol weight not set";
			if(isNaN(this.sampleWeight)) throw "Sample weight not set";
			if(isNaN(this.refMolWeight)) throw "Reference compound mol weight not set";
			if(isNaN(this.refWeight)) throw "Reference compound weight not set";
			if(isNaN(this.refPurity)) throw "Reference compound purity not set";
			if(this.refAssignment == 0) throw "Reference compound has not been assigned";

			if(assignment == 0) {
				return NaN;
			}

			var assignmentArea    = (this.useGSD ? assignment.gsdValue         : assignment.integralValue);
			var refAssignmentArea = (this.useGSD ? this.refAssignment.gsdValue : this.refAssignment.integralValue);


			//calculate purity for sample assignment
			var numerator   =   assignmentArea * this.refAssignment.nuclideCount * this.sampleMolWeight * this.refWeight;
					// -----------------------------------------------------------------------------------------------------------
			var denominator =     refAssignmentArea * assignment.nuclideCount  * this.refMolWeight * this.sampleWeight;

			return (denominator == 0.0) ? NaN : (numerator / denominator) * this.refPurity;
		}


		/**
		 * Calculate salt molar ratio, or returns NaN if no salt integral or sample integrals have been specified.
		 */
		this.saltRatio = function() {

			if(this.saltAssignment == 0) {
				return NaN;
			}

			//find average area (normalied) per proton for all sample integrals
			var totalAreaPerProton = 0;
			var count = 0;
			for(var i=0; i<this.sampleAssignments.length; i++) {
				if(this.sampleAssignments[i] != 0) {
					var assignmentArea = (this.useGSD ? this.sampleAssignments[i].gsdValue : this.sampleAssignments[i].integralValue);

					totalAreaPerProton += (assignmentArea / this.sampleAssignments[i].nuclideCount);

					count++;
				}
			}
			if(count == 0) {
				return NaN;
			}
			var averageAreaPerProton = totalAreaPerProton / count;

			//calculate ratio
			var saltAssignmentArea = (this.useGSD ? this.saltAssignment.gsdValue : this.saltAssignment.integralValue);
			var saltAreaPerProton = saltAssignmentArea / this.saltAssignment.nuclideCount;
			return saltAreaPerProton / averageAreaPerProton;
		}


		/**
		 * Load an instance of this class from a file (properties loaded into this instance).
		 * @param file - a File object for the file to load data from
		 * @param spec - an NMRSpectrum object to obtain integral information from
		 */
		this.loadFromFile = function(file, spec) {
			if( ! file.exists) {
				throw "File: "+file+" could not be found.";
			}
			file.open(File.ReadOnly);
			var reader = TextStream(file);

			while( ! reader.atEnd()) {
				var line = reader.readLine();
				var pos = line.indexOf("=");
				if(pos > 0) {
					var paramName = line.substring(0, pos);
					var paramValue = line.substring(pos+1);

					if(paramName == "refCompound") {
						this[paramName] = deserialiseReferenceCompound(paramValue);
					} else if(paramName.indexOf("Assignment") > 0) {

						//first check if GSD information is needed and if it is available
						if(this.useGSD && Application.version.revision >= 7569 && (spec.gsdPeaks() == undefined || !spec.gsdPeaks().count)) {
							mainWindow.doAction("nmrGSD");
							if(spec.gsdPeaks() == undefined || !spec.gsdPeaks().count) {
								mainWindow.doAction("nmrGSD");
							}
						}

						if(paramName.substring(paramName.length-1) == "s") {
							var a = deserialiseAssignment(paramValue, spec);
							if(a != 0) {
								this[paramName].push(a);
							}
						} else {
							this[paramName] = deserialiseAssignment(paramValue, spec);
						}
					} else if(typeof(this[paramName]) == "boolean") {
						this[paramName] = (paramValue == "true");
					} else if(typeof(this[paramName]) == "number") {
						this[paramName] = parseFloat(paramValue);
					} else if(typeof(this[paramName]) == "string") {
						this[paramName] = paramValue;
					} else {
						print(paramName + " param not found");
					}
				}
			}
			file.close();
		}

		/**
		 * Save the properties of this instance to a file so that it can be re-created later.
		 */
		this.saveToFile = function(file) {
			file.open(File.WriteOnly);
			var writer = TextStream(file);

			writer.write("useGSD=" + (this.useGSD ? "true" : "false") + "\n");
			writer.write("sampleMolWeight=" + this.sampleMolWeight + "\n");
			writer.write("sampleWeight=" + this.sampleWeight + "\n");
			writer.write("refCompound=" + this.refCompound.serialise() + "\n");
			writer.write("refCmpdName=" + this.refCmpdName + "\n");
			writer.write("refMolWeight=" + this.refMolWeight + "\n");
			writer.write("refPurity=" + this.refPurity + "\n");
			writer.write("refWeight=" + this.refWeight + "\n");
			if(this.refAssignment != 0) {
				writer.write("refAssignment=" + this.refAssignment.serialise() + "\n");
			}
			for(var i=0; i<this.sampleAssignments.length; i++) {
				if(this.sampleAssignments[i] != 0) {
					writer.write("sampleAssignments="+this.sampleAssignments[i].serialise() + "\n");
				}
			}
			if(this.saltAssignment != 0) {
				writer.write("saltAssignment=" + this.saltAssignment.serialise() + "\n");
			}
			writer.write("reportOnSeparatePage=" + (this.reportOnSeparatePage ? "true" : "false") + "\n");

			file.close();
		}
	}


/**
 * Prompts the user for data about the spectrum and then calculates the purity of the sample.
 */
function calculatePurity() {

	//prompt user for params
	var p = getPurityParameters();
	if(p == undefined) {
		return;	//user cancelled operation
	}


	//decide where to display the results
	var page = undefined;
	try {
		var w = new DocumentWindow(Application.mainWindow.activeWindow());
		if(p.reportOnSeparatePage) {
			//create new page
			var oldPageCount = w.pageCount();
			w.setCurPageIndex(oldPageCount-1);
			Application.mainWindow.doAction("action_Edit_CreateNewPage");
			page = new Page(w.page(oldPageCount));
		} else {
			//get current page
			page = new Page(w.curPage());
		}
	} catch(err) {}	//leave page undefined


	//display purity
	var msg = "Average Purity = <b>" + floatSigFigs(p.averagePurity(), 2) + "%</b>" + (aShowErrorBounds ? " Â± 2%" : "") + "<br/>";
	msg += p.useGSD ? "Using Global Spectral Deconvolution  (GSD) instead of integral values.<br/>" : "";
	msg += "Assuming sample weight: " + p.sampleWeight + " mg, and mol weight: " + p.sampleMolWeight + "<br/>";
	msg += "Using Reference Compound: " + p.refCmpdName + " (" + p.refWeight + " mg, " + p.refPurity+ "% purity, Mol Weight=" + p.refMolWeight + ")<br/>";
	for(var i=0; i<p.sampleAssignments.length; i++) {
		msg += "Sample Integral "+(i+1)+": " + p.sampleAssignments[i].toString() + " ("+p.sampleAssignments[i].nuclideCount+" nuclides) - Purity = "+floatSigFigs( p.purityOf(p.sampleAssignments[i]) ,1)+"%" + (aShowErrorBounds ? " Â± 2%" : "") + "<br/>";
	}
	msg += "Reference Integral: "+p.refAssignment.toString()+" ("+p.refAssignment.nuclideCount+" nuclides)";

	displayMessage( msg, "Purity Calculation Result", page, true);


	//display salt molar ratio
	var saltRatio = p.saltRatio();
	if( ! isNaN(saltRatio)) {
		msg = "Average Salt Molar Ratio = <b>1 : " + floatSigFigs(saltRatio, 2) + "</b><br/>";
		msg += "Salt Integral: ("+p.saltAssignment.toString()+" ("+p.saltAssignment.nuclideCount+" nuclides)";

		displayMessage( msg, "Salt Ratio Calculation Result", page, false);
	}
}


/**
 * Display the result of a calculation; either by displaying a message box or by creating a text box on a given page.
 * @param msg - message to display to the user
 * @param title - title of the message to display
 * @param page - the Page object to display the message on (if undefined then display message box)
 * @param alignLeft - if true align the textbox on the left of the page, otheriwse align it to the right
 */
function displayMessage(msg, title, page, alignLeft) {

	if(page == undefined) {

		MessageBox.information(msg);

	} else {

		Application.mainWindow.activeWindow().setCurPage(page);

		var textBox = draw.text(msg, title, title, true);

		textBox.top = page.top + (page.height / 2);
		textBox.bottom = textBox.top + 50;
		if(alignLeft) {
			textBox.left = page.left + 5;
			textBox.right = textBox.left + 175;
		} else {
			textBox.left = page.right - 180;
			textBox.right = textBox.left + 175;
		}
		Application.mainWindow.activeWindow().update();
	}
}


/**
 * Prompt user for parameters for purity calculation/
 */
function getPurityParameters() {

	//get current page and NMR spectrum
	var page = mainWindow.activeDocument.curPage();
	var spec = nmr.activeSpectrum();
	if(spec.spectra == undefined) {
		 MessageBox.critical("No spectrum found on current page.");
		 return;
	}

	//load reference compounds from file
	var refCmpds = getReferenceCompounds();

	//create new object to hold parameters
	var params = new PurityCalcParams();
	params.sampleMolWeight = floatSigFigs(getMolWeightOfMoleculeOn(page),4);

	//get initial parameters from user
	var diag = new PurityCalcParamsDialog(params, refCmpds);
	if( ! diag.show()) {
		return undefined;
	}

	//if user specified file to load parameters from, load them
	if(diag.paramsFile != undefined) {
		params.loadFromFile(diag.paramsFile, spec);

		//confirm parameters with user
		var diag = new PurityCalcParamsDialog(params, refCmpds);
		if( ! diag.show()) {
			return undefined;
		}
	}

	//if GSD required but not applied yet, then apply them now
	var gsdPeaks = undefined;
	if(params.useGSD && Application.version.revision >= 7569){
		if(spec.gsdPeaks() == undefined || !spec.gsdPeaks().count)
			mainWindow.doAction("nmrGSD");
		gsdPeaks = spec.gsdPeaks();
	}

	//enter manual integration mode
	mainWindow.doAction("nmrIntegrateManual");


	//keep showing dialog and prompting for integrals until all necessary parameters have been collected
	while( params.refAssignment == 0 || params.sampleAssignments.length == 0 || (diag.doSaltCalculation && params.saltAssignment == 0) ) {

		//get reference integral
		if(params.refAssignment == 0) {
			var integrals = getIntegrals(spec, "Create Integral for Reference Compound..", 1)
			if(integrals.length == 0) {
				return undefined;
			} else {
				params.refAssignment = new Assignment(integrals[0], gsdPeaks);
			}
		}

		//get sample integrals
		if(params.sampleAssignments.length == 0) {
			integrals = getIntegrals(spec, "Create Integrals for Sample..", 5)
			if(integrals.length == 0) {
				return undefined;
			} else {
				for(var i=0; i<integrals.length; i++) {
					params.sampleAssignments[i] = new Assignment(integrals[i], gsdPeaks);
				}
			}
		}

		//get salt integral is user wants to do a salt molar ration calculation
		if(diag.doSaltCalculation && params.saltAssignment == 0) {
			integrals = getIntegrals(spec, "Create Integral for Salt..", 1)
			if(integrals.length == 0) {
				return undefined;
			} else {
				params.saltAssignment = new Assignment(integrals[0], gsdPeaks);
			}
		}

		//show user latest set of parameters for purity calculation
		diag = new PurityCalcParamsDialog(params, refCmpds);
		if( ! diag.show()) {
			return undefined;
		}
	}

	//exit Manual integration mode
	mainWindow.doAction("drwModeSelect");

	//save parameters to local file on users machine
	try {
		params.saveToFile(new File(aPurityCalcParamsFile));
		settings.setValue(aPurityCalcParamsFileKey,aPurityCalcParamsFile);
	} catch(err) {MessageBox.information(err)}	//ignore errors

	return params;
}


/**
 * Class to encapsulate the behaviour of a dialog that collects parameters for a purity calculation.
 */
function PurityCalcParamsDialog(params, refCmpds) {

	this.diag = new Dialog();
	this.diag.title = "Purity Calculator Options";
	this.refCmpds = refCmpds;
	this.params = params;
	this.paramsFile = undefined;

	//offer to load params from file
	this.loadParamsBox = new GroupBox();
	this.loadParamsBox.title = "Load Previous Parameters";
	this.loadParamsButton = new PushButton();
	this.loadParamsButton.text = "Load Parameters From File";
	this.loadParamsButton.clicked.connect(this, function() {
		var methodFilename = FileDialog.getOpenFileName("Text Files (*.txt)", "Enter file to load parameters from");
		if(methodFilename == null || methodFilename == "") {
			return; 		//user cancelled operation
		}
		this.paramsFile = new File(methodFilename);
		this.diag.accept();
	});
	this.loadParamsBox.add(this.loadParamsButton);
	this.usePrevParamsButton = new PushButton();
	this.usePrevParamsButton.text = "Use Parameters From Previous Calculation";
	this.usePrevParamsButton.clicked.connect(this, function() {
		//check that params file exists on client machine
		this.paramsFile = new File(aPurityCalcParamsFile);
		if(this.paramsFile.exists) {
			this.diag.accept();
		} else {
			this.paramsFile = undefined;
			MessageBox.information("Parameters from a previous purity calculation could not be found.");
		}
	});
	this.loadParamsBox.add(this.usePrevParamsButton);
	this.diag.add(this.loadParamsBox);


	//sample details
	this.sampleOptionsBox = new GroupBox();
	this.sampleOptionsBox.title = "Sample Details";
	this.smplMolWeightText = new LineEdit();
	this.smplMolWeightText.label = "Sample Mol Weight"
	this.sampleOptionsBox.add(this.smplMolWeightText);
	this.smplAmountText = new LineEdit();
	this.smplAmountText.label = "Weight of Sample (mg)"
	this.sampleOptionsBox.add(this.smplAmountText);
	this.diag.add(this.sampleOptionsBox);

	//reference details
	this.refsmplOptionsBox = new GroupBox();
	this.refsmplOptionsBox.title = "Reference Compound Details";
	//
	//this.leRefCmpdFile = new LineEdit();
	//this.lbRefCmpdFile.label = "Reference Compounds File";
	this.btRefCmpdFile = new PushButton();
	this.btRefCmpdFile.text = "Load Compound Parameters File";
	this.btRefCmpdFile.clicked.connect(this, function() {

	var compoundsFilename = FileDialog.getOpenFileName("Csv Files (*.csv)", "Enter file to load compounds from");
		if( compoundsFilename == null ||  compoundsFilename == "") {
			return; 		//user cancelled operation

		}
		aPurityReferenceCompoundFile=compoundsFilename;
		settings.setValue(aPurityReferenceCompoundFileKey,aPurityReferenceCompoundFile);
		refCmpds = getReferenceCompounds();
		for(var i=0; i<refCmpds.length; i++) {
			this.refCmpdCombo.addItem(refCmpds[i]);
		}
	});
	this.refsmplOptionsBox.add(this.btRefCmpdFile);

	//
	this.refCmpdCombo = new ComboBox();
	for(var i=0; i<refCmpds.length; i++) {
		this.refCmpdCombo.addItem(refCmpds[i]);
	}
	this.refCmpdCombo['activated(int)'].connect(this, function() {
		//change molWeight/purity values when different compound is selected
		var ref = refCmpds[this.refCmpdCombo.currentIndex];
		this.refCmpdNameText.text = ref.compound;
		this.refCmpdNameText.visible = (ref.compound == "Other");
		this.refMolWeightText.text = (isNaN(ref.molWeight) ? "" : ref.molWeight);
		this.refMolWeightText.visible = (ref.compound == "Other");
		this.refPurityText.text = ref.purity;
	});
	this.refsmplOptionsBox.add(this.refCmpdCombo);
	this.refCmpdNameText = new LineEdit();
	this.refCmpdNameText.label = "Reference Compound Name"
	this.refCmpdNameText.visible = false;
	this.refsmplOptionsBox.add(this.refCmpdNameText);
	this.refMolWeightText = new LineEdit();
	this.refMolWeightText.label = "Reference Compound Mol Weight"
	this.refMolWeightText.visible = false;
	this.refsmplOptionsBox.add(this.refMolWeightText);
	this.refPurityText = new LineEdit();
	this.refPurityText.label = "Reference Compound Purity (%)"
	this.refsmplOptionsBox.add(this.refPurityText);
	this.refAmountText = new LineEdit();
	this.refAmountText.label = "Weight of Reference Compound (mg)"
	this.refsmplOptionsBox.add(this.refAmountText);
	this.diag.add(this.refsmplOptionsBox);

	//use GSD tick-box
	this.useGsdCheckbox = new CheckBox();
	this.useGsdCheckbox.text = "Use GSD as well as integration";
	this.diag.add(this.useGsdCheckbox);

	//do salt ratio as well tick-box
	this.doSaltRadioCheckbox = new CheckBox();
	this.doSaltRadioCheckbox.text = "Calculate salt molar ratio as well";
	this.diag.add(this.doSaltRadioCheckbox);
	this.doSaltCalculation = false;

	//reference compound integral and nuclide count
	this.refAssignmentCtrl = new AssignmentControl("Reference Compound Integral" );
	this.diag.add(this.refAssignmentCtrl.ctrl);

	//sample integral and nuclide counts
	this.smplAssignmentCtrls = [];
	this.smplAssignmentBox = new GroupBox();
	this.smplAssignmentBox.title = "Sample Integrals";
	this.diag.add(this.smplAssignmentBox);

	//salt integral and nuclide count
	this.saltAssignmentCtrl = new AssignmentControl("Salt Molar Ratio Determination" );
	this.diag.add(this.saltAssignmentCtrl.ctrl);

	//recalculate button
		this.reCalcButton = new PushButton();
		this.reCalcButton.text = "Re-Calculate";
		this.reCalcButton.clicked.connect(this, function() {
			this.collectParams();
			for(var i=0; i<this.smplAssignmentCtrls.length; i++) {
				this.smplAssignmentCtrls[i].updateTitle();
			}
		});
		this.diag.add(this.reCalcButton);


	//save parameters button
	this.saveParamsButton = new PushButton();
	this.saveParamsButton.text = "Save Parameters";
	this.saveParamsButton.clicked.connect(this, function() {
		try {
			var methodFilename = FileDialog.getSaveFileName("Text Files (*.txt)", "Enter filename to save as");
			if(methodFilename == null || methodFilename == "") {
				return; 		//user cancelled operation
			}
			this.params.saveToFile(new File(methodFilename));
			MessageBox.information("Done!");
		} catch(err) {
			MessageBox.information("Error saving parameters: "+err);
		}
	});
	this.diag.add(this.saveParamsButton);

	//report on seperate page tick-box
	this.reportOnNewPageCheckbox = new CheckBox();
	this.reportOnNewPageCheckbox.text = "Add report on new page";
	this.diag.add(this.reportOnNewPageCheckbox);


	/**
	 * Updates all GUI components in this dialog and then shows it and waits dialog is closed.
	 * @returns true if user clicked OK or false otherwise.
	 */
	this.show = function() {

		//fill all fields with data
		this.update();

		//show dialog and return whether or not user clicked ok
		return this.diag.exec();
	}


	/**
	 * Event handler for window close. Populates the params object.
	 */
	this.diag['finished(int)'].connect(this, function(e) {
		this.collectParams();
	});


	/**
	 * Updates the parameter values with values from the GUI components.
	 */
	this.collectParams = function() {
		params.sampleMolWeight = parseFloat(this.smplMolWeightText.text);
		params.sampleWeight = parseFloat(this.smplAmountText.text);
		params.refCompound = refCmpds[this.refCmpdCombo.currentIndex];
		params.refCmpdName = this.refCmpdNameText.text;
		params.refMolWeight = parseFloat(this.refMolWeightText.text);
		params.refPurity = parseFloat(this.refPurityText.text);
		params.refWeight = parseFloat(this.refAmountText.text);
		if(this.refAssignmentCtrl != []) {
			params.refAssignment = this.refAssignmentCtrl.getAssignment();
		} else {
			params.refAssignment = 0;
		}
		params.sampleAssignments = [];
		if(this.smplAssignmentCtrls.length > 0) {
			for(var i=0; i<this.smplAssignmentCtrls.length; i++) {
				if(this.smplAssignmentCtrls[i].getAssignment() != 0) {
					params.sampleAssignments.push( this.smplAssignmentCtrls[i].getAssignment() );
				}
			}
		}
		if(this.saltAssignmentCtrl != []) {
			params.saltAssignment = this.saltAssignmentCtrl.getAssignment();
		} else {
			params.saltAssignment = 0;
		}
		params.useGSD = this.useGsdCheckbox.checked;
		params.reportOnSeparatePage = this.reportOnNewPageCheckbox.checked;
		this.doSaltCalculation = this.doSaltRadioCheckbox.checked;
	};

	/**
	 * Updates the GUI components with the parameter values.
	 */
	this.update = function() {
		this.smplMolWeightText.text = (isNaN(this.params.sampleMolWeight) ? "" : this.params.sampleMolWeight);
		this.smplAmountText.text = (isNaN(this.params.sampleWeight) ? "" : this.params.sampleWeight);
		var selectedIndex = 0;
		if(this.params.refCompound != 0) {
			for(var i=0; i<this.refCmpds.length; i++) {
				if( this.params.refCompound.compound == this.refCmpds[i].compound) {
					selectedIndex = i;
				}
			}
		}
		this.smplAssignmentCtrls = [];
		this.refCmpdCombo.currentIndex = selectedIndex;
		this.refCmpdNameText.text = (this.params.refCmpdName == "") ? this.refCmpds[this.refCmpdCombo.currentIndex].compound : this.params.refCmpdName;
		this.refCmpdNameText.visible = (this.refCmpds[this.refCmpdCombo.currentIndex].compound == "Other");
		this.refMolWeightText.text = (isNaN(this.params.refMolWeight) ? this.refCmpds[this.refCmpdCombo.currentIndex].molWeight : this.params.refMolWeight);
		this.refMolWeightText.visible = (this.refCmpds[this.refCmpdCombo.currentIndex].compound == "Other");
		this.refPurityText.text = (isNaN(this.params.refPurity) ? this.refCmpds[this.refCmpdCombo.currentIndex].purity : this.params.refPurity);
		this.refAmountText.text = (isNaN(this.params.refWeight) ? "" : this.params.refWeight);
		this.refAssignmentCtrl.smplAssignmentCtrls = this.smplAssignmentCtrls;
		this.refAssignmentCtrl.assignmentAltered = function(assignment) {		//add event handler for assignmentAltered event of AssignmentControl
			for(var i=0; i<this.smplAssignmentCtrls.length; i++) {
				this.smplAssignmentCtrls[i].updateTitle();
			}
			return this.ctrl.title;
		};
		this.refAssignmentCtrl.setAssignment(this.params.refAssignment);
		for(var i=0; i<this.smplAssignmentCtrls.length; i++) {
			this.smplAssignmentCtrls[i].ctrl.visible = false;
		}
		if(this.smplAssignmentCtrls.length == 0) {
			for(var i=0; i<this.params.sampleAssignments.length; i++) {
				var ac = new AssignmentControl("");
				ac.params = this.params;
				ac.assignmentAltered = function(assignment) {		//add event handler for assignmentAltered event of AssignmentControl
					var purity = this.params.purityOf( assignment );
					if(isNaN(purity)) {
						return "";
					} else if(purity > 105) {
						return "!!! Purity = " + floatSigFigs(purity,2) + "% !!!";
					} else {
						return "Purity = " + floatSigFigs(purity,2) + "%";
					}
				};
				this.smplAssignmentCtrls[i] = ac;
				this.smplAssignmentBox.add(ac.ctrl);
				ac.setAssignment( this.params.sampleAssignments[i] );
			}
		} else {
			for(var i=0; i<this.params.sampleAssignments.length; i++) {
				this.smplAssignmentCtrls[i].setAssignment( this.params.sampleAssignments[i] );
			}
		}
		this.smplAssignmentBox.visible = (this.smplAssignmentCtrls.length > 0);
		this.saltAssignmentCtrl.setAssignment(this.params.saltAssignment);

		this.useGsdCheckbox.checked = this.params.useGSD;
		this.useGsdCheckbox.visible = (Application.version.revision >= 7569) && (this.smplAssignmentCtrls.length == 0);
		this.doSaltRadioCheckbox.visible = (this.smplAssignmentCtrls.length == 0);
		this.loadParamsBox.visible = (this.smplAssignmentCtrls.length == 0);
		this.saveParamsButton.visible = (this.smplAssignmentCtrls.length > 0);
		this.reportOnNewPageCheckbox.visible = (this.smplAssignmentCtrls.length > 0);
		this.reportOnNewPageCheckbox.checked = this.params.reportOnSeparatePage;
	}
}



/**
 * Class to encapsulate a GUI control that can edit an Assignment object.
 */
function AssignmentControl( userMsg ) {

	this.assignment = 0;

	this.ctrl = new GroupBox();
	this.ctrl.title = userMsg;
	this.ctrl.checkable=true;
	this.ctrl.checked = true;
	this.integralText = new LineEdit();
	this.integralText.label = "Integral";
	this.ctrl.add(this.integralText);
	this.nuclideCountSpin = new SpinBox();
	this.nuclideCountSpin.minimum = 1;
	this.nuclideCountSpin.maximum = 99;
	this.nuclideCountSpin.value = 1;
	this.nuclideCountSpin['valueChanged(int)'].connect(this, function() {
		if(this.assignment != 0) {
			this.assignment.nuclideCount = this.nuclideCountSpin.value;
			this.updateTitle();
		}
	});
	this.ctrl.add("Nuclide Count ", this.nuclideCountSpin);


	/**
	 * Event that is fired whenever the nuclideCountSpin control is modified or setAssignment() function is called.
	 * @returns a new value for the title of this control.
	 */
	this.assignmentAltered = function(assignment) {
		return this.ctrl.title;
	}


	/**
	 * Gets the assignment that this control is editing.
	 */
	this.getAssignment = function() {
		return (this.ctrl.checked) ? this.assignment : 0;
	}

	/**
	 * Sets the assignment that this control is editing, and update the GUI components.
	 * @param assignment - an instance of the Assignment class.
	 */
	this.setAssignment = function(assignment) {
		this.ctrl.checked = true;
		this.assignment = assignment;
		if(assignment == 0) {
			this.ctrl.visible = false;
		} else {
			this.ctrl.visible = true;
			this.integralText.text = assignment.toString();
			this.nuclideCountSpin.value = assignment.nuclideCount;
			this.updateTitle();
		}
	}

	/**
	 * Updated the title of the GroupBox in this control by raising an assignmentAltered() event and using the responce.
	 */
	this.updateTitle = function() {
		this.ctrl.title = this.assignmentAltered(this.assignment);	//fire assignmentAltered event
	}
}


/**
 * Class to encapsulate the properties of an integral and the numebr of nuclides it corresponds to.
 */
function Assignment(integral, gsdPeaks) {

	if(integral == undefined) {
		this.integralValue = NaN;
		this.integralMin = NaN;
		this.integralMax = NaN;
		this.gsdValue = NaN;
	} else {
		this.integralValue = integral.integralValue(integral.normValue);
		this.integralMin = Math.min(integral.rangeMin(1), integral.rangeMax(1));
		this.integralMax = Math.max(integral.rangeMin(1), integral.rangeMax(1));

		if(gsdPeaks == undefined) {
			this.gsdValue = NaN;
		} else {
			this.gsdValue = 0.0;
			for(var i=0; i<gsdPeaks.count; i++) {
				var thisPeak = gsdPeaks.at(i);
				if(thisPeak.center < this.integralMax && thisPeak.center > this.integralMin) {
					this.gsdValue += thisPeak.area;
				}
			}
		}
	}
	this.nuclideCount = 1;

	/**
	 * Returns a string representation of this object.
	 */
	this.toString = function() {
		if(isNaN(this.integralMin) || isNaN(this.integralMax)) {
			return "-";
		} else {
			var str = floatSigFigs(this.integralMin, 5) + " - " + floatSigFigs(this.integralMax, 5) + " ppm, value = " + floatSigFigs(this.integralValue, 5);
			if( ! isNaN(this.gsdValue)) {
				str += ", gsd = "+floatSigFigs(this.gsdValue, 5);
			}
			return str;
		}
	}

	/**
	 * Serialises the properties of this instance to a string that can be parsed to re-create this instance.
	 */
	this.serialise = function() {
		if(isNaN(this.integralMin) || isNaN(this.integralMax)) {
			return "";
		} else {
			return this.integralMin + "\t" + this.integralMax + "\t" + this.integralValue + "\t" + this.nuclideCount;
		}
	}
}
/**
 * Re-create an instance of the Assignment class from a string obtained by the serialise() function.
 * @param line - the string containing the properties of the new instance
 * @param spec - an NMRSpectrum object to obtain integral information from
 * @returns a new instance of the Assignment class
 */
function deserialiseAssignment(line, spec) {
	var parts = line.split("\t");
	if(parts.length == 4) {
		try {
			var min = parseFloat(parts[0]);
			var max = parseFloat(parts[1]);

			//get GSD information if available
			var gsdPeaks = (Application.version.revision >= 7569) ? spec.gsdPeaks() : undefined;

			//try to find existing integral
			var intList = new Integrals(spec.integrals());
			for(var i=0; i<intList.count; i++) {
				if(intList.at(i).rangeMin(1) == min && intList.at(i).rangeMax(1) == max) {
					intList.at(i).normValue = intList.normValue;

					obj = new Assignment(intList.at(i), gsdPeaks);
					obj.nuclideCount = parseInt(parts[3]);
					return obj;
				}
			}

			//otherwise, add new integral
			var newInt = new Integral( spec, new SpectrumRegion(min, max), false );
			newInt.normValue = intList.normValue;
			intList.append(newInt);
			spec.setIntegrals(intList);
			spec.process();

			obj = new Assignment(newInt, gsdPeaks);
			obj.nuclideCount = parseInt(parts[3]);
			return obj;

		} catch(err) {}
	}
	return 0;
}


/**
 * Class to encapsulate the properties of a reference compound.
 */
function ReferenceCompound() {
	this.compound = "";
	this.molWeight = 0.0;
	this.purity = "";


	/**
	 * Returns a string representation of this object.
	 */
	this.toString = function() {
		return this.compound;
	}

	/**
	 * Serialises the properties of this instance to a string that can be parsed to re-create this instance.
	 */
	this.serialise = function() {
		return this.compound + "\t" + this.molWeight + "\t" + this.purity;
	}
}
/**
 * Re-create an instance of the ReferenceCompound class from a string obtained by the serialise() function.
 * @param line - the string containing the properties of the new instance
 * @returns a new instance of the ReferenceCompound class
 */
function deserialiseReferenceCompound(line) {
	var parts = line.split("\t");
	if(parts.length == 3) {
		try {
			var obj = new ReferenceCompound();
			obj.compound = parts[0];
			obj.molWeight = parseFloat(parts[1]);
			obj.purity = parseFloat(parts[2]);
			return obj;
		} catch(err) {}
	}
	return [];
}



/**
 * Return the molecular weight of the molecule on the given page, or 0 if no molecule is found.
 * @param page - the Page object of the page to find a molecule from
 */
function getMolWeightOfMoleculeOn(page) {

	//if there is no molecule on this page, return 0
	if( page.itemCount("Molecule") == 0 ) {

		return 0;

	//otherwise, if there is at least one molecule
	} else {

		//get the first molecule on the page
		var mol = new Molecule(page.item(0, "Molecule"));

		/*
		//sum up the number of protons in each atom to get mol weight
		var moleculeHcount = 0;
		for(var i=0; i<=mol.atomCount; i++) {
			if(mol.atom(i).nHAll > 0) {
				moleculeHcount += mol.atom(i).nHAll;
			}
		}
		return moleculeHcount;
		*/

		return mol.molecularWeight();
	}
}


/**
 * Prompts the user to create some new integrals and returns them.
 * @param spec - the NMRSpectrum object of the spectrum to select an integral from
 * @param userPrompt - the message to display to the user instructing them what to do
 * @return an Integral object for the selected new integral or 0 if no integral was selected
 */
function getIntegrals(spec, userPrompt, maxIntegralCount) {

	//keep prompting user until they enter an integral or cancel the operation
	var newIntegrals = [];
	while( newIntegrals.length == 0 ) {

		//copy spectrum integrals before user has control
		var prevIntList = new Integrals(spec.integrals());

		//wait for user to create new intregral
		if(0 == Application.mainWindow.waitForInput(userPrompt, "Select Integral")) {

			break;	//return empty if user cancels operation
		}

		//copy list of integrals after user has (hopefully) created a new one
		var newIntList = new Integrals(spec.integrals());

		//if there are not more integrals than there were before then user did not create an integral object
		if( prevIntList.count >= newIntList.count ) {
			MessageBox.information("New integral could not be found. Please create a new integral to continue.");

		//else if user has created too many integrals, ask them to do them again
		} else if( (newIntList.count - prevIntList.count) > maxIntegralCount ) {
			MessageBox.information("This operation requires only "+maxIntegralCount+" integral(s)\nPlease create no more than this number.");

		//otherwise return all new integrals
		} else {

			for(var i=prevIntList.count; i<newIntList.count; i++) {
				newIntegrals[i - prevIntList.count] = newIntList.at( i );
				newIntegrals[i - prevIntList.count].normValue = newIntList.normValue;
			}
			return newIntegrals;
		}

		//reset integral list back to original list
		spec.setIntegrals(prevIntList);
		spec.process();
	}
	return newIntegrals;
}


/**
 * Loads details of possible reference compounds from a csv file.
 * @returns an array of objects, each with a "compound" and "purity" property
 */
function getReferenceCompounds() {

	var results = new Array();

	if(File.exists(aPurityReferenceCompoundFile)) {
		var count = 0;
		var textFile = new File(aPurityReferenceCompoundFile);
		textFile.open(File.ReadOnly);
		var reader = TextStream(textFile);
		reader.readLine();	//ignore header line
		while( ! reader.atEnd()) {
			var line = reader.readLine();
			var params = line.split(",");
			if(params.length >= 2) {

				var ref = new ReferenceCompound();
				ref.compound = params[0];
				ref.molWeight = parseFloat(params[1]);
				ref.purity = params[2];

				results[count++] = ref;

			} else {
				MessageBox.critical("expected compound and purity on line "+(count+2));
			}
		}
		textFile.close();
	} else {
		//throw "reference compound data not found.";

		var ref = new ReferenceCompound();
		ref.compound = "Other";
		ref.molWeight = "";
		ref.purity = "";
		results[0] = ref;
	}

	return results;
}


/**
 * Rounds a floating point number up to a specified number significant figures.
 */
function floatSigFigs(f, sigFigs) {
	var factor = Math.pow(10, sigFigs);
	return Math.round(factor*f)/factor;
}

}

if (this.MnUi && MnUi.scripts_nmr) {
	MnUi.scripts_nmr.scripts_nmr_PurityCalculator = PurityCalculator;
}
